#!/usr/bin/env python
#
#       EarthquakeAlert.py
#       
#       Copyright 2008 Dionisis Dimakopoulos <dionx@users.sourceforge.com>
#       
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of the GNU General Public License as published by
#       the Free Software Foundation; either version 2 of the License, or
#       (at your option) any later version.
#       
#       This program is distributed in the hope that it will be useful,
#       but WITHOUT ANY WARRANTY; without even the implied warranty of
#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#       GNU General Public License for more details.
#       
#       You should have received a copy of the GNU General Public License
#       along with this program; if not, write to the Free Software
#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#       MA 02110-1301, USA.

import sys, os, warnings, webbrowser, time, pickle
import xml.dom.minidom, urllib2

"""Don't display warnings notifying that a screen could not be found 
when running the script from a text only terminal """
warnings.filterwarnings("ignore", "could not open display")
warnings.filterwarnings("ignore", "gdk_screen_get_width")

import pygtk
pygtk.require('2.0')
import gtk, gobject

class Earthquake:
	""" This class represents an earthquake """
	def __init__(self, title, description, link):
		self.title = title
		self.description = description
		self.link = link
		#time, latitude, longitude, depth and revision fields are 
		#created by __parseInfo
		self.__parseInfo()
	
	def equals(self, quake=None):
		""" Checks against the link of the other earthquake to find out
		whether it is the same event """
		if quake!=None:
			if self.link == quake.link:
				return True
		return False

	def isAfter(self, quake=None):
		"""Check if the current event is later than the event in the 
		quake object. By convention, when there is no quake object given
		the method returns True."""
		if quake==None:
			return True
		if self.time>quake.time:
			return True
		return False
		
	def __parseInfo(self):
		"""Parses the information from the title and the description of 
		the event in order to discover the properties:
		magnitude, area, time, latitude, longitude, depth, revision"""
		magnitude=self.title.split(",")[0]
		self.magnitude=magnitude.split(" ")[1]
		self.area="".join(self.title.split(",")[1:]).strip()
		descriptionParts=self.description.split()
		self.time=time.strptime(descriptionParts[0]+" "+
			descriptionParts[1],"%Y-%m-%d %H:%M:%S")
		self.latitude=descriptionParts[2]+" "+descriptionParts[3]
		self.longitude=descriptionParts[4]+" "+descriptionParts[5]
		self.depth=descriptionParts[6]
		self.revision=descriptionParts[8]

	def __str__(self):
		revised=self.revision
		if revised=="M":
			revised = "Manually revised"
		elif revised=="A":
			revised = "Automatically calculated"
		else:
			revised = "Confirmed"
		output = time.strftime("%x %X", self.time)\
			+ "\nMagnitude: " + self.magnitude\
			+ "\t Depth: " + self.depth + " km"\
			+ "\nArea: "+self.area\
			+ " (" +self.latitude + " " + self.longitude + ")"\
			+ "\n" + revised
		return output
		

class EarthquakeAlert:
	"""A tool that alerts for new earthquakes around the world. It is 
	reading an RSS feed from GFZ Potsdam - Earthquake Bulletin 
	(http://geofon.gfz-potsdam.de/db/eqinfo.php). The application can 
	be configured to retrieve either the generic RSS feed, that 
	contains information about the last 20 earthquakes around the world,
	or a more specific RSS feed containing the events inside a range
	defined by the latitude and longitude of two points."""

	__name__ = 'EarthquakeAlert'
	__version__ = '0.4'
	__author__ = 'Dionisis Dimakopoulos'
	__desc__ =__doc__

	__confFilename="earthquakeAlert.conf"
	__validConfStr = ["datemin", "datemax", "latmin","latmax", "lonmin", 
			"lonmax", "magmin", "nmax", "generic"]
	
	def __init__(self, verbose=False, showIcon=False):
		self.__verbose = verbose
		self.__showIcon = showIcon
		if verbose: print "Reading configuration"
		self.confValues = self.readConfiguration()
		if verbose: print "Reading last event"
		self.lastEvent = self.readLastEvent()
		self.__url = self.defineRSSUrl()
		self.refreshInfo()
		if self.returnVal!=4:
			print "No new event."
			if verbose:
				print "Last event was:"
		else:
			print "New earthquake!"
		if self.returnVal==4 or verbose:
			print self.events[0]
			print "For more information: " + self.events[0].link
		self.writeLastEventToDisk(self.events[0])
		if showIcon and self.returnVal==4:
			try:
				icon = NotificationIcon(self.events[0], self)
				#refresh every 5 mins if the user hasn't reviewed the 
				#last event
				refresh=gobject.timeout_add(300000, self.refreshIcon)
				gtk.main()
			except:
				if __verbose:
					print "In text mode, no icon will be shown"

	def refreshInfo(self):
		"""Downloads the RSS feed, from the specified url, creates
		Earthquake objects from the events and checks if there is a new
		event. By convention the application announces a new event the 
		first time it runs, or if the lastEvent file is deleted"""
		if self.__verbose: print "Downloading RSS Feed"
		content=self.getRSS(self.__url)
		if self.__verbose: print "Parsing RSS Feed"
		self.events = self.rss2Objects(content)
		self.returnVal=0
		if self.lastEvent == None:
			self.returnVal=4
		else:
			if not self.events[0].equals(self.lastEvent):
				if self.events[0].isAfter(self.lastEvent):
					self.returnVal=4
	
	def refreshIcon(self):
		"""Callback function for the gobject timeout. Needs to return
		True for the gobject timer to restart."""
		self.refreshInfo()
		return True

	def defineRSSUrl(self):
		"""Constructs the url of the RSS from the configuration file
		values"""
		genericRSS="http://geofon.gfz-potsdam.de/db/eqinfo.php?fmt=rss"
		url="http://geofon.gfz-potsdam.de/db/eqinfo.php?"+\
			"datemin="+self.confValues[0]+ \
			"&datemax="+self.confValues[1]+ \
			"&latmin="+self.confValues[2]+\
			"&latmax="+self.confValues[3]+\
			"&lonmin="+self.confValues[4]+\
			"&lonmax="+self.confValues[5]+\
			"&magmin="+self.confValues[6]+\
			"&nmax="+self.confValues[7]+\
			"&fmt=rss"
		if self.confValues[self.__validConfStr.index("generic")]=="True":
			url=genericRSS	
		return url
			
	def readLastEvent(self):
		"""Reads the last event from the disk, if there is a pickled 
		event already stored, otherwise returns None"""
		lastEvent = None
		try:
			if os.path.isfile('lastEvent'):
				f = open('lastEvent','r')
				lastEvent=pickle.load(f)
		except IOError:
			if __verbose:
				print "lastEvent file could not be read"
		return lastEvent

	def readConfiguration(self):
		"""Reads the values from configuration file. There is a list
		with the default values hardcoded, in case the configuration
		file is not present""" 
		confValues=["", "", "32","42", "16", "30", "", "20", "False"]
		if os.path.isfile(self.__confFilename):
			f = open(self.__confFilename, "r")
			for line in f:
				#ignore empty lines
				if len(line)>1:	
					#ignore comments
					if line.strip()[0] != "#":		
						if line.count("#")>0:
							#ignore comments at the end of the line
							line = line.split("#")[0] 	
						parts = line.strip().split("=")
						if parts[0] in self.__validConfStr:
							if self.validateConfAttribute(parts[0], parts[1]):
								confValues[self.__validConfStr.index(parts[0])]=parts[1]
							else:
								self.invalidConfMessage(line, parts[0], parts[1])
		return confValues

	def invalidConfMessage(self, line, attr, val):
		print "The configuration file is corrupt:"
		print line 
		print "Value "+val+" is not valid for attribute "+attr
		deleteLockFile()
		sys.exit(1)
		
	def validateConfAttribute(self, confAttribute, confValue):
		isValid = True
		if confAttribute == "nmax":
			if confAttribute.isdigit():
				try:
					tempInt=int(confValue)
				except:
					isValid = False
		if confAttribute in ["latmin", "latmax", "lonmin", "lonmax", "magmin"]:
			isValid = self.isValidFloat(confValue)
		if confAttribute in ["datemin", "datemax"]:
			if confValue!="":
				isValid = self.isValidTime(confValue.strip())
		if confAttribute == "generic":
			if confValue=="True" or confValue=="False":
				isValid=True
			else:
				isValid=False
		return isValid
	
	def isValidFloat(self, str):
		isValid=True
		try:
			f=float(str)
		except:
			isValid=False
		return isValid

	def isValidTime(self, str):
		isValid = True
		try:
			t = time.strptime(str,"%Y-%m-%d")
		except:
			isValid=False
		return isValid
		
	def getRSS(self, url):
		conn = urllib2.urlopen(url)
		lines = ""
		for line in conn:
			lines = lines + line 
		return lines
	
	def rss2Objects(self, content):
		"""Parses the xml returned using the DOM parser"""
		try:
			rss = xml.dom.minidom.parseString(content)
		except xml.parsers.expat.ExpatError:
			print "No data found. Check the configuration file and try again"
			if self.__verbose:
				print "Content returned:"
				print content
			deleteLockFile()
			sys.exit(1)
		items=rss.getElementsByTagName("item")
		return self.items2Objects(items)
	
	def items2Objects(self, items):
		events = []
		for item in items:
			title = self.getText(item.getElementsByTagName("title")[0].childNodes)
			desc = self.getText(item.getElementsByTagName("description")[0].childNodes)
			link = self.getText(item.getElementsByTagName("link")[0].childNodes)
			event = Earthquake(title, desc, link)
			events.append(event)
		return events

	def getText(self,nodelist):
		rc = ""
		for node in nodelist:
			if node.nodeType == node.TEXT_NODE:
				rc = rc + node.data
		return rc
		
	def writeLastEventToDisk(self, lastEvent):
		"""Pickles the last event for later use"""
		f = open('lastEvent', 'w')
		pickle.dump(lastEvent, f)
		

class NotificationIcon:
	"""This class represents the notification icon of the application.
	The icon is shown when it is specified in the arguments and there is
	a new event"""
	def __init__(self, quake, mainApp):
		self.quake = quake
		self.mainApp = mainApp
		self.icon = self.createIcon()
		self.icon.set_visible(True)
		
	def cb_popup_menu(self, widget, button, time, data = None):
		if button == 3:
			if data:
				data.show_all()
             	data.popup(None, None, None, 3, time)
	
	def createIcon(self):
		icon = gtk.StatusIcon()
		menu = self.createMenu()
		icon.set_from_file("earthquakeAlert.png")
		icon.set_tooltip("New earthquake\nClick for info")
		icon.connect("activate", self.cb_activate)
		icon.connect('popup-menu', self.cb_popup_menu, menu)
		return icon
		
	def createMenu(self):
		"""This is the popup menu appearing when the user right clicks
		on the notification icon"""
		menu = gtk.Menu()
		menuItem = gtk.MenuItem("Show details")
		menuItem.connect('activate', self.cb_activate_icon, self, "Details")
		menu.append(menuItem)
		menuItem = gtk.MenuItem("Previous events")
		submenu = self.createSubmenu()
		menuItem.set_submenu(submenu)
		menu.append(menuItem)
		menuItem = gtk.SeparatorMenuItem()
		menu.append(menuItem)
		menuItem = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
		menuItem.connect('activate', self.cb_activate_icon, self, "About")
		menu.append(menuItem)
		menuItem = gtk.ImageMenuItem(gtk.STOCK_QUIT)
		menu.append(menuItem)
		menuItem.connect('activate', self.cb_activate_icon, self, "Quit")
		return menu

	def createSubmenu(self):
		"""This submenu contains the last events from the RSS feed"""
		menu = gtk.Menu()
		for event in self.mainApp.events:
			menuItem = gtk.MenuItem(event.title)
			menuItem.connect("activate", self.cb_activate_icon, self, event.link)
			menu.append(menuItem)
		return menu
		
	def cb_activate_icon(self, widget, icon=None, data = None):
		if data=="Quit":
			gtk.main_quit()
		elif data=="About":
			self.showAboutBox()
		elif data=="Details":
			icon.showWindow(self.quake)
		elif data.index("http")==0:
			webbrowser.open(data)

	def showAboutBox(self):
		aboutBoxText = u"<b>Earthquake Alert 0.5</b>"\
			+ u"\nCopyright \xA9 2008, Dionisis Dimakopoulos\n"\
			+ u"\nThis program is free software; you can redistribute"\
			+ u"it and/or modify it under the terms of the GNU General"\
			+ u" Public License as published by the Free Software "\
			+ u"Foundation; either version 2 of the License, or (at "\
			+ u"your option) any later version."
		aboutBox = gtk.MessageDialog(
			parent = None,
			buttons = gtk.BUTTONS_OK,
			message_format = "")
		aboutBox.set_markup(aboutBoxText)
		aboutBox.run()
		aboutBox.destroy()
		

		
	def cb_activate(self, status_icon, data=None):
		self.showWindow(self.quake)
		
	def cb_button(self, button, data=None):
		webbrowser.open(self.quake.link)
		
	def showWindow(self, quake):
		"""Creates the window that contains the information related 
		with the last event"""
		window = gtk.Window()
		window.set_title("New earthquake information")
		vbox = gtk.VBox()
		label=gtk.Label(str(quake))
		button=gtk.Button("More info",None , True)
		button.set_tooltip_text("Opens a new browser window")
		button.connect("clicked", self.cb_button)
		checkbox = gtk.CheckButton("Exit on close")
		checkbox.set_active(True)	#By default close app on exit
		vbox.pack_start(label)
		vbox.pack_start(button)
		vbox.pack_start(checkbox)
		label.show()
		button.show()
		checkbox.show()
		window.add(vbox)
		vbox.show()
		screen = window.get_screen()
		width, height = window.get_size()
		window.move((screen.get_width()-width)/2, (screen.get_height()-height)/2)
		window.connect("destroy", self.destroy, checkbox)
		window.show()
	
	def destroy(self, widget, data=None):
		if data.get_active():	#if checkbox is checked
			gtk.main_quit()
				
def main():
	usageStr="""Usage: earthquakeAlert [--verbose] [--show_icon] [--help]
	--verbose		Be verbose
	--show_icon		Show the notification icon if there is a new event
	--help			Show this screen
	"""
	
	validArgs=["verbose", "help", "show_icon"]
	openLockFile()
	verbose=False
	showIcon=False
	gtkAvailable=False
	if gtk.gdk.screen_width() != 0:
		gtkAvailable=True
	for arg in sys.argv:
		if arg[:2] == "--":
			arg=arg[2:]
			if arg in validArgs:
				if arg=="verbose": verbose=True
				if arg=="help":	
					print usageStr
					deleteLockFile()
					return 3
				if arg=="show_icon": 
					if gtkAvailable: 
						showIcon=True
					else:
						if verbose:
							print "show_icon cannot be used in text mode"
			else:
				print "Error: Unknown argument"
				print usageStr
				deleteLockFile()
				return 1
	alert = EarthquakeAlert(verbose, showIcon)
	deleteLockFile()
	return alert.returnVal

def openLockFile():
	if os.path.isfile('lock'):
		print "Another instance of the application is running. If \
you are sure there is no other instance running, delete the lock file \
from the application folder"
		sys.exit(2)		
	else:
		lockFile = open('lock', 'w')

def deleteLockFile():
	if os.path.isfile("lock"):
		os.remove("lock")
		
if __name__ == '__main__': sys.exit(main())
